/*
 * Syntelos 'SX'
 * Copyright (C) 1998, 2007  John Pritchard.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301 USA.
 */
package syntelos.sx;

import syntelos.lang.Time;
import syntelos.sys.Capabilities;

import alto.io.Authentication;
import alto.io.Input;
import alto.io.Output;
import alto.io.Principal;
import alto.io.Uri;
import alto.lang.Buffer;
import alto.lang.Date;
import alto.lang.OutputStream;
import alto.lang.Protocol;
import alto.lang.Status;
import alto.lang.Type;
import alto.sys.IO;

/**
 * Server write response. 
 * 
 * <h3>Authentication</h3>
 * 
 * <b>N.B.</b> Response- Methods are defined as
 * 
 * <pre>
 * if (this.isAuthenticated()){
 *   // (ok)
 * }
 * </pre>
 * 
 * and <i>not</i>
 * 
 * <pre>
 * if (req.isAuthenticated()){
 *   // (!)
 * }
 * </pre>
 * 
 * 
 * @author jdp
 * @since 1.6
 */
public abstract class Response 
    extends syntelos.lang.HttpResponse
    implements java.lang.Cloneable,
               IO.Target
{

    private static syntelos.sx.methods.BadRequest BadRequest;
    public final static syntelos.sx.methods.BadRequest BadRequest(){
        if (null == BadRequest)
            BadRequest = new syntelos.sx.methods.BadRequest();
        return BadRequest;
    }
    private static syntelos.sx.methods.Conflict Conflict;
    public final static syntelos.sx.methods.Conflict Conflict(){
        if (null == Conflict)
            Conflict = new syntelos.sx.methods.Conflict();
        return Conflict;
    }
    private static syntelos.sx.methods.Unauthorized Unauthorized;
    public final static syntelos.sx.methods.Unauthorized Unauthorized(){
        if (null == Unauthorized)
            Unauthorized = new syntelos.sx.methods.Unauthorized();
        return Unauthorized;
    }
    private static syntelos.sx.methods.NotImplemented NotImplemented;
    public final static syntelos.sx.methods.NotImplemented NotImplemented(){
        if (null == NotImplemented)
            NotImplemented = new syntelos.sx.methods.NotImplemented();
        return NotImplemented;
    }
    private static syntelos.sx.methods.NotFound NotFound;
    public final static syntelos.sx.methods.NotFound NotFound(){
        if (null == NotFound)
            NotFound = new syntelos.sx.methods.NotFound();
        return NotFound;
    }
    private static syntelos.sx.methods.UnknownProtocol UnknownProtocol;
    public final static syntelos.sx.methods.UnknownProtocol UnknownProtocol(){
        if (null == UnknownProtocol)
            UnknownProtocol = new syntelos.sx.methods.UnknownProtocol();
        return UnknownProtocol;
    }
    private static syntelos.sx.methods.RequestEntityTooLarge RequestEntityTooLarge;
    public final static syntelos.sx.methods.RequestEntityTooLarge RequestEntityTooLarge(){
        if (null == RequestEntityTooLarge)
            RequestEntityTooLarge = new syntelos.sx.methods.RequestEntityTooLarge();
        return RequestEntityTooLarge;
    }
    private static syntelos.sx.methods.ServiceUnavailable ServiceUnavailable;
    public final static syntelos.sx.methods.ServiceUnavailable ServiceUnavailable(){
        if (null == ServiceUnavailable)
            ServiceUnavailable = new syntelos.sx.methods.ServiceUnavailable();
        return ServiceUnavailable;
    }
    private static syntelos.sx.methods.Error Error;
    public final static syntelos.sx.methods.Error Error(){
        if (null == Error)
            Error = new syntelos.sx.methods.Error();
        return Error;
    }

    private Output out;
    private long time_exit, time_elapsed, txlength;
    protected boolean committed;
    private byte[] trace;

    public Response(){
        super();
    }


    public final Uri getUri(){
        Request request = this.getRequest();
        if (null != request)
            return request.getUri();
        else
            return null;
    }
    /**
     * @return True immediately after the response line is written
     * (HTTP 1.0 or greater), and before the remainder of the head is
     * written.
     */
    public final boolean isCommitted(){
        return this.committed;
    }
    public final boolean isNotCommitted(){
        return (!this.committed);
    }
    @Override
    public Request getRequest(){
        return (Request)this.request;
    }
    /**
     * {@link Server} "error" init.
     */
    public final Response error(Socket socket)
        throws java.io.IOException
    {
        if (null != socket){
            this.protocol = Protocol.HTTP10;
            this.socket = socket;
            this.out = socket.openOutput();
            this.clearHeaders();
            this.setDate();
            this.setHeader(syntelos.sx.Headers.Server);
            this.setPersistent(false);
            return this;
        }
        else
            throw new IllegalArgumentException();
    }
    /**
     * {@link Service} "error" init.
     */
    public final Response error(Socket socket, Request request)
        throws java.io.IOException
    {
        if (null == request)
            return this.error(socket);
        else if (null != socket){
            this.init(request,socket.openOutput());
            this.setPersistent(false);
            return this;
        }
        else
            throw new IllegalArgumentException();
    }
    /**
     * <p> This method defines basic response status and headers.  It
     * is called by {@link Request#respond()}, which is called from
     * {@link Service}.  </p>
     * 
     * <p> This method does not perform I/O on the principal socket
     * (argument output stream).  It may perform other I/O in
     * establishing the headers that will be sent by the {@link
     * #service()} method after this method returns. </p>
     */
    public final void init(Request request, Output out)
        throws java.io.IOException
    {
        if (null != request && null != out){
            this.request = request;
            this.method = request.getMethod();
            this.protocol = request.getProtocol();
            this.out = out;
            this.clearHeaders();
            this.setDate();
            this.setHeader(syntelos.sx.Headers.Server);
            this.setHeader(syntelos.sx.Headers.Method.SX);
            this.setHeader("X-Log",request.getReferenceLogRequestPath());
            this.setPersistent(request);
            this.setLocation(request);
            return ;
        }
        else
            throw new IllegalArgumentException();
    }
    /**
     * <p> This method is overridden in subclasses to define status,
     * headers and content.  It is called after {@link #init} by
     * {@link Request#respond}.  </p>
     */
    public abstract void respond(Request request)
        throws java.io.IOException,
               java.lang.InterruptedException;

    /**
     * Called by {@link Service}.  
     */
    final boolean write()
        throws java.io.IOException
    {
        if (this.committed)
            return this.isPersistent();
        else {
            this.committed = true;
            try {
                this.writeMessage(this.out);
                return this.isPersistent();
            }
            catch (java.io.IOException ignore){
                /*
                 * Connection reset by peer
                 */
                throw new Ignore(ignore);
            }
            finally {
                long exit = java.lang.System.currentTimeMillis();
                this.time_exit = exit;
                Stats.Hertz.Instance.update(exit);
                Request request = this.getRequest();
                if (null != request){
                    long enter = request.getTimeEnter();
                    long elapsed = (exit-enter);
                    this.time_elapsed = elapsed;
                    Stats.Response.Instance.update(elapsed);
                }
            }
        }
    }

    /**
     * Response programmer's invocation of authentication. 
     */
    public final boolean isAuthenticated(){
        return this.isAuthenticated(Authentication.Any);
    }
    /**
     */
    public final boolean isAuthenticated(Authentication required){
        if (null == required)
            throw new alto.sys.Error.Bug();
        
        else {
            Authentication authenticationMethod = this.authenticationMethod;
            if (null != authenticationMethod)
                return required.equals(authenticationMethod);

            else {
                Request request = this.getRequest();
                if (null != request){
                    try {
                        if (request.isAuthenticating())
                            request.authenticate();

                        if (request.isAuthenticated()){
                            Principal.Actual principal = (Principal.Actual)request.getPrincipal();
                            alto.io.Authentication auth = principal.getAuthentication();

                            this.authenticationMethod = auth;

                            this.setHeader("X-Authenticated",String.valueOf(auth)+' '+principal.getName());

                            return required.equals(auth);
                        }
                        else
                            return false;
                    }
                    catch (java.io.IOException exc){
                        throw new alto.sys.Error(exc);
                    }
                }
                else
                    throw new alto.sys.Error.Bug();
            }
        }
    }
    public final boolean isAuthenticated(Authentication with, Principal as){
        if (this.isAuthenticated(with)){
            Principal has = this.request.getPrincipal();
            return (has.equals(as));
        }
        else
            return false;
    }
    public final boolean isAuthenticated(Capabilities capabilities){
        if (this.isAuthenticated()){
            if (null != capabilities){
                Principal.Actual principal = (Principal.Actual)this.request.getPrincipal();
                return capabilities.accept(principal);
            }
        }
        return false;
    }
    public final boolean isAuthenticated(Authentication with, Capabilities capabilities){
        if (this.isAuthenticated(with)){
            if (null != capabilities){
                Principal.Actual principal = (Principal.Actual)this.request.getPrincipal();
                return capabilities.accept(principal);
            }
        }
        return false;
    }
    public final boolean isAuthenticated(Authentication with, Capabilities capabilities, 
                                         Principal.Authentic owner)
    {
        if (this.isAuthenticated(with)){
            Principal.Actual caller = (Principal.Actual)this.request.getPrincipal();
            if (null != capabilities){
                return (capabilities.accept((Principal.Actual)owner,caller));
            }
        }
        return false;
    }
    /**
     * Message pre processor called from {@link
     * syntelos.lang.HttpMessage#writeMessage}
     */
    protected final void messageFormat(){
        this.headline = (this.getProtocol().toString()+' '+this.status);
    }
    /**
     * @return Request method is GET or HEAD, path is "...[^/]" without
     * type, for possible ".../" directory redirect.
     */
    public final boolean canRedirectDir(){
        Request request = this.getRequest();
        if (null != request){
            if (request.hasNotType()){
                String name = this.getMethod();
                if ((("GET".equals(name))||("HEAD".equals(name)))){
                    Type pathType = request.getType();
                    if (null == pathType){
                        String path = request.getPath();
                        return ('/' != path.charAt(path.length()-1));
                    }
                    else
                        return false;
                }
                else
                    return false;
            }
            else
                return false;
        }
        else
            throw new alto.sys.Error.Bug();
    }
    public final void redirectToPath(String path){
        String url = Request.Location(this.getRequest(),path);
        this.redirectToUrl(url);
    }
    public final Response cloneResponse(){
        try {
            Response clone = (Response)this.clone();

            return clone;
        }
        catch (CloneNotSupportedException exc){
            throw new alto.sys.Error(exc);
        }
    }
    /**
     * Clone the response with a more useful status message.  If the
     * status is (subsequently) changed on the cloned response (as in
     * response init), the message string is preserved while the
     * status codes change.
     * 
     * @see Application#createResponse(syntelos.sx.Request)
     * @see Method#BadRequest
     * @see Method#Error
     */
    public final Response cloneResponse(String msg){
        Response clone = this.cloneResponse();
        if (null != msg)
            clone.status = new syntelos.lang.Status(this.status,msg);
        return clone;
    }
    public final long getTimeExit(){
        return this.time_exit;
    }
    public final long getTimeElapsed(){
        return this.time_elapsed;
    }
    public final String getTimeElapsedMillis(){
        return java.lang.String.valueOf(this.time_elapsed);
    }
    public final String getTimeElapsedSeconds(){
        return Time.ToString(this.time_elapsed);
    }
    public final byte[] getTrace(){
        return this.trace;
    }
    public final int getTraceLength(){
        byte[] trace = this.trace;
        if (null != trace)
            return trace.length;
        else
            return 0;
    }
    public final long getTransactionLength(Request request){
        return this.txlength;
    }
    public long lastModified(){
        return 0L;
    }
    public java.nio.channels.ReadableByteChannel openChannelReadable(){
        return null;
    }
    public java.nio.channels.ReadableByteChannel getChannelReadable(){
        return null;
    }
    public java.io.InputStream openInputStream()
        throws java.io.IOException
    {
        return null;
    }
    public Input openInput()
        throws java.io.IOException
    {
        return null;
    }
    public java.io.InputStream getInputStream(){
        return null;
    }
    public Input getInput(){
        return null;
    }
    public java.nio.channels.WritableByteChannel openChannelWritable()
        throws java.io.IOException
    {
        if (this.committed)
            return null;
        else
            return super.openChannelWritable();
    }
    public java.nio.channels.WritableByteChannel getChannelWritable(){
        if (this.committed)
            return null;
        else
            return super.getChannelWritable();
    }
    public java.io.OutputStream openOutputStream()
        throws java.io.IOException
    {
        if (this.committed)
            return null;
        else
            return super.openOutputStream();
    }
    public Output openOutput()
        throws java.io.IOException
    {
        if (this.committed)
            return null;
        else
            return super.openOutput();
    }
    public java.io.OutputStream getOutputStream()
        throws java.io.IOException
    {
        if (this.committed)
            return null;
        else
            return super.getOutputStream();
    }
    public Output getOutput()
        throws java.io.IOException
    {
        if (this.committed)
            return null;
        else
            return super.getOutput();
    }
    /**
     * Called from {@link Service}.  Should never throw any exception.
     */
    public final void close(){
        try {
            this.socket = null;
            long txlength = 0;
            OutputStream out = (OutputStream)this.out;
            if (null != out){
                this.out = null;

                byte[] trace = out.getTrace();
                if (null != trace){
                    this.trace = trace;
                    txlength += trace.length;
                }
            }
            /*
             */
            Request request = this.getRequest();
            if (null != request){
                request.close();
                txlength += request.getTransactionLengthRequest();
            }
            /*
             */
            this.txlength = txlength;
        }
        finally {
            this.request = null;
        }
    }
}
